package org.therandoms.textgen;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
 * This is the class for a random text generator. It will allow you to do the
 * following:
 * <ul>
 * <li>Add a production to a production category</li>
 * <li>Remove a production from a production category</li>
 * <li>Add a production category</li>
 * <li>Remove a production category</li>
 * <li>Create random text</li>
 * <li>Get a list of production categories</li>
 * <li>Get a list of productions</li>
 * </ul>
 *
 * @author Logan Widick
 * @author Vinh Pham
 */
public class TextGeneratorDictionary<T extends Serializable> implements
		Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = -4657795368343610817L;

	/**
	 * The default maximum # of times that the text generator can recurse.
	 */
	public static final int DEFAULT_MAX_RECURRENCES = 50;

	/**
	 * Stores the entries in the dictionary
	 */
	private HashMap<T, List<Production<T>>> dictionaryEntries = new HashMap<T, List<Production<T>>>();

	/**
	 * Used for randomly selecting stuff
	 */
	private Random mRandom = new Random();

	/**
	 * Adds a production to a production category. This will create the category
	 * if it does not already exist.
	 * 
	 * @param production
	 *            the word/phrase/symbol/whatever that can be produced
	 * @param productionCategory
	 *            the name of the production category
	 */
	public void addProductionToCategory(Production<T> production,
			T productionCategory) {
		// defensive programming
		if (production == null || productionCategory == null
				|| production.isEmpty()) {
			return;
		}

		// get production ArrayList for category
		List<Production<T>> productionsForCategory = dictionaryEntries
				.get(productionCategory);
		if (productionsForCategory == null) {
			addProductionCategory(productionCategory);
			productionsForCategory = dictionaryEntries.get(productionCategory);
		}

		// add to productions
		productionsForCategory.add(production);

		// save
		dictionaryEntries.put(productionCategory, productionsForCategory);
	}

	/**
	 * Removes a production from a production category
	 * 
	 * @param production
	 *            the list of words/phrases/symbols/whatever that can be
	 *            produced
	 * @param productionCategory
	 *            the name of the production category
	 */
	public void removeProductionFromCategory(Production<T> production,
			T productionCategory) {
		// defensive programming
		if (production == null || productionCategory == null
				|| production.isEmpty()) {
			return;
		}

		// get production ArrayList for category
		List<Production<T>> productionsForCategory = dictionaryEntries
				.get(productionCategory);
		if (productionsForCategory == null) {
			return;
		}

		// remove from productions
		productionsForCategory.remove(production);

		// save
		dictionaryEntries.put(productionCategory, productionsForCategory);
	}

	/**
	 * Adds a production category
	 * 
	 * @param productionCategory
	 *            the name of the production category
	 */
	public void addProductionCategory(T productionCategory) {
		if (productionCategory == null) {
			return;
		}
		if (dictionaryEntries.get(productionCategory) == null) {
			ArrayList<Production<T>> newList = new ArrayList<Production<T>>();
			dictionaryEntries.put(productionCategory, newList);
		}
	}

	/**
	 * Removes a production category
	 * 
	 * @param productionCategory
	 *            the name of the production category
	 */
	public void removeProductionCategory(T productionCategory) {
		if (productionCategory == null) {
			return;
		}
		dictionaryEntries.remove(productionCategory);
	}

	/**
	 * Generates text by appending to a StringBuffer.
	 * 
	 * @param targets
	 *            the list of string targets to use. These can be terminal
	 *            symbols, production categories, or some combination thereof.
	 * @param randomOutput
	 *            The list to put the randomly generated stuff in
	 */
	public void generateText(List<T> targets, List<T> randomOutput) {
		generateText(targets, randomOutput, false);
	}

	/**
	 * Generates text by appending to a StringBuffer.
	 * 
	 * @param targets
	 *            the list of string targets to use. These can be terminal
	 *            symbols, production categories, or some combination thereof.
	 * @param randomOutput
	 *            The list to put the randomly generated stuff in
	 * @param randomlyHandleProductionCategory
	 *            If true, and if the production chosen matches the name of a
	 *            production category, then randomly decide whether to treat it
	 *            as a terminal symbol/word/phrase or a production category. If
	 *            true, and if the production chosen does NOT match the name of
	 *            a production category, then treat it as a terminal
	 *            symbol/word/phrase. If false, always treat production category
	 *            names as production categories, and treat everything else as
	 *            terminal symbols/words/phrases.
	 */
	public void generateText(List<T> targets, List<T> randomOutput,
			boolean randomlyHandleProductionCategory) {
		generateText(targets, randomOutput, randomlyHandleProductionCategory,
				DEFAULT_MAX_RECURRENCES);
	}

	/**
	 * Generates text by appending to a StringBuffer.
	 * 
	 * @param targets
	 *            the list of string targets to use. These can be terminal
	 *            symbols, production categories, or some combination thereof.
	 * @param randomOutput
	 *            The list to put the randomly generated stuff in
	 * @param randomlyHandleProductionCategory
	 *            If true, and if the production chosen matches the name of a
	 *            production category, then randomly decide whether to treat it
	 *            as a terminal symbol/word/phrase or a production category. If
	 *            true, and if the production chosen does NOT match the name of
	 *            a production category, then treat it as a terminal
	 *            symbol/word/phrase. If false, always treat production category
	 *            names as production categories, and treat everything else as
	 *            terminal symbols/words/phrases.
	 * @param numRecurrences
	 *            The maximum number of times that recursion can occur.
	 */
	public void generateText(List<T> targets, List<T> randomOutput,
			boolean randomlyHandleProductionCategory, int numRecurrences) {
		while (!targets.isEmpty()) {
			T nextEntry = targets.get(0);

			// determine if entry is a production category
			List<Production<T>> productions = dictionaryEntries.get(nextEntry);

			// Terminal symbol; append
			if (productions == null) {
				randomOutput.add(nextEntry);
			}

			/*
			 * Production category that was randomly selected to serve as a
			 * terminal symbol; append
			 */
			else if (randomlyHandleProductionCategory && !mRandom.nextBoolean()) {
				randomOutput.add(nextEntry);
			}
			// Other production entry;
			else {
				if (productions.size() > 0) {
					selectProduction(productions, randomOutput,
							randomlyHandleProductionCategory, numRecurrences);
				} else {
					randomOutput.add(nextEntry);
				}
			}
			targets.remove(0);
		}
	}

	/**
	 * Generates possible production categories for a production.
	 * 
	 * @param production
	 *            the production
	 * @return the production categories for the production
	 */
	private Set<T> generateProductionCategoriesForProduction(
			Production<T> production) {
		if (production == null) {
			return null;
		}

		LinkedHashSet<T> retSet = new LinkedHashSet<T>();
		for (T str : production.getSymbols()) {
			// ... add to possible production categories if it exists
			if (dictionaryEntries.containsKey(str)) {
				retSet.add(str);
			}
		}

		if (retSet.isEmpty()) {
			return null;
		} else
			return retSet;
	}

	/**
	 * Generates text by appending to a StringBuffer.
	 * 
	 * @param targets
	 *            the list of string targets to use. These can be terminal
	 *            symbols, production categories, or some combination thereof.
	 * @param randomOutput
	 *            the list to use for random output.
	 * @param randomlyHandleProductionCategory
	 *            If true, and if the production chosen matches the name of a
	 *            production category, then randomly decide whether to treat it
	 *            as a terminal symbol/word/phrase or a production category. If
	 *            true, and if the production chosen does NOT match the name of
	 *            a production category, then treat it as a terminal
	 *            symbol/word/phrase. If false, always treat production category
	 *            names as production categories, and treat everything else as
	 *            terminal symbols/words/phrases.
	 * @param numRecurrences the number of times to recursively call this function. Must be > 0. 
	 */
	private void selectProduction(List<Production<T>> targets,
			List<T> randomOutput, boolean randomlyHandleProductionCategory,
			int numRecurrences) {
		if (numRecurrences <= 0) {
			return;
		}
		if (targets.size() <= 0) {
			return;
		}
		Production<T> randomEntry = targets
				.get(mRandom.nextInt(targets.size()));

		// determine if entry is a production category
		Set<T> productionCategories = generateProductionCategoriesForProduction(randomEntry);
		if (productionCategories == null) {
			randomEntry.appendSymbolsToList(randomOutput);
			return;
		}

		// otherwise, go through the production categories
		for (T cat : productionCategories) {
			// if random indicates that it should be a terminal, append it.
			if (randomlyHandleProductionCategory && !mRandom.nextBoolean()) {
				randomEntry.appendSymbolsToList(randomOutput);
			}

			// otherwise, continue recursion
			else {
				try {
					List<Production<T>> nextProductionList = dictionaryEntries
							.get(cat);
					// ensure there are productions for the category, and further recursion is ok.
					if (nextProductionList.size() > 0 && numRecurrences > 1) {
						selectProduction(nextProductionList, randomOutput,
								randomlyHandleProductionCategory,
								numRecurrences - 1);
					}

					// fallback: append as a terminal symbol
					else {
						randomOutput.add(cat);
					}
				} catch (StackOverflowError mErr) {
					randomOutput.add(cat);
				}
			}			
		}
	}

	/**
	 * Gets the production categories available.
	 * 
	 * @return the production categories.
	 */
	public Set<T> getProductionCategories() {
		return dictionaryEntries.keySet();
	}

	/**
	 * Gets all possible productions
	 * 
	 * @return all possible productions
	 */
	public Set<Production<T>> getProductions() {
		HashSet<Production<T>> retSet = new HashSet<Production<T>>();
		for (T key : dictionaryEntries.keySet()) {
			retSet.addAll(dictionaryEntries.get(key));
		}

		return retSet;
	}

	/**
	 * Gets the productions for a production category
	 * 
	 * @param productionCategory
	 *            the production category
	 * @return the productions for the production category
	 */
	public List<Production<T>> getProductionsForCategory(T productionCategory) {
		return dictionaryEntries.get(productionCategory);
	}
}
